﻿using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace FlexField.Metadata
{
    /// <summary>
    /// Provides the APIs for managing metadata types and fields.
    /// </summary>
    /// <typeparam name="TKey">The type used for the primary key for the fields and entities.</typeparam>
    public class MetadataManager<TKey> : IDisposable where TKey : IEquatable<TKey>
    {
        private bool _disposed;

        /// <summary>
        /// Constructs a new instance of <see cref="MetadataManager{TKey}"/>.
        /// </summary>
        /// <param name="entityStore">The persistence store the manager will operate entities over.</param>
        /// <param name="fieldStore">The persistence store the manager will operate fields over.</param>
        /// <param name="optionsAccessor">The accessor used to access the <see cref="MetadataOptions"/>.</param>
        /// <param name="entityValidators">A collection of validators for types.</param>
        /// <param name="fieldValidators">A collection of validators for fields.</param>
        /// <param name="errors">The <see cref="MetadataErrorDescriber"/> used to provider error messages.</param>
        /// <param name="logger">The logger used to log messages, warnings and errors.</param>
        /// <exception cref="ArgumentNullException"></exception>
        public MetadataManager(IEntityStore<TKey> entityStore, IFieldStore<TKey> fieldStore,
            IOptions<MetadataOptions> optionsAccessor,
            IEnumerable<IEntityValidator<TKey>> entityValidators, IEnumerable<IFieldValidator<TKey>> fieldValidators,
            MetadataErrorDescriber errors, ILogger<MetadataManager<TKey>> logger)
        {
            EntityStore = entityStore ?? throw new ArgumentNullException(nameof(entityStore));
            FieldStore = fieldStore ?? throw new ArgumentNullException(nameof(fieldStore));
            Options = optionsAccessor?.Value ?? new MetadataOptions();
            ErrorDescriber = errors;
            Logger = logger;

            if (entityValidators != null)
            {
                foreach (var v in entityValidators)
                {
                    EntityValidators.Add(v);
                }
            }

            if (fieldValidators != null)
            {
                foreach (var v in fieldValidators)
                {
                    FieldValidators.Add(v);
                }
            }
        }

        /// <summary>
        /// The cancellation token used to cancel operations.
        /// </summary>
        protected virtual CancellationToken CancellationToken => CancellationToken.None;

        /// <summary>
        /// Gets the persistence store that metadata types operates over.
        /// </summary>
        protected IEntityStore<TKey> EntityStore { get; }

        /// <summary>
        /// Gets the persistence store that metadata fields operates over.
        /// </summary>
        protected IFieldStore<TKey> FieldStore { get; }

        /// <summary>
        /// Gets a list of validators for entities to call before persistence.
        /// </summary>
        public IList<IEntityValidator<TKey>> EntityValidators { get; } = new List<IEntityValidator<TKey>>();

        /// <summary>
        /// Gets a list of validators for types to call before persistence.
        /// </summary>
        public IList<IFieldValidator<TKey>> FieldValidators { get; } = new List<IFieldValidator<TKey>>();

        /// <summary>
        /// Gets the <see cref="MetadataErrorDescriber"/> used to provider error messages.
        /// </summary>
        /// <value>
        /// The <see cref="MetadataErrorDescriber"/> used to provider error messages.
        /// </value>
        public MetadataErrorDescriber ErrorDescriber { get; set; }

        /// <summary>
        /// The <see cref="MetadataOptions"/> used to configure metadata.
        /// </summary>
        public MetadataOptions Options { get; set; }

        /// <summary>
        /// Gets the <see cref="ILogger"/> used to log messages from the manager.
        /// </summary>
        /// <value>
        /// The <see cref="ILogger"/> used to log messages from the manager.
        /// </value>
        public ILogger Logger { get; set; }

        /// <summary>
        /// Releases all resources used by the role manager.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases the unmanaged resources used by the role manager and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing && !_disposed)
            {
                EntityStore.Dispose();
                FieldStore.Dispose();
            }
            _disposed = true;
        }

        /// <summary>
        /// Throws if this class has been disposed.
        /// </summary>
        protected void ThrowIfDisposed()
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(GetType().Name);
            }
        }

        /// <summary>
        /// Should return <see cref="MetadataResult.Success"/> if validation is successful. This is
        /// called before saving the type via CreateEntity or UpdateEntity.
        /// </summary>
        /// <param name="entity">The metadata entity to be validated.</param>
        /// <returns>A <see cref="MetadataResult"/> representing whether validation was successful.</returns>
        protected virtual async Task<MetadataResult> ValidateEntityAsync(MetadataEntity<TKey> entity)
        {
            var errors = new List<MetadataError>();
            foreach (var v in EntityValidators)
            {
                var result = await v.ValidateAsync(this, entity);
                if (!result.Succeeded)
                {
                    errors.AddRange(result.Errors);
                }
            }

            if (errors.Count > 0)
            {
                Logger.LogWarning(LoggerEventIds.TypeValidationFailed, $"Type validation failed: {string.Join(";", errors.Select(e => e.Code))}.");
                return MetadataResult.Failed(errors.ToArray());
            }
            return MetadataResult.Success;
        }

        /// <summary>
        /// Should return <see cref="MetadataResult.Success"/> if validation is successful. This is
        /// called before saving the field via CreateType or UpdateType.
        /// </summary>
        /// <param name="entityId">The primary key of the type that the field belongs to.</param>
        /// <param name="field">The metadata field to be validated.</param>
        /// <returns>A <see cref="MetadataResult"/> representing whether validation was successful.</returns>
        protected virtual async Task<MetadataResult> ValidateFieldAsync(TKey entityId, MetadataField<TKey> field)
        {
            var errors = new List<MetadataError>();
            foreach (var v in FieldValidators)
            {
                var result = await v.ValidateAsync(this, entityId, field);
                if (!result.Succeeded)
                {
                    errors.AddRange(result.Errors);
                }
            }

            if (errors.Count > 0)
            {
                Logger.LogWarning(LoggerEventIds.FieldValidationFailed, $"Field validation failed: {string.Join(";", errors.Select(e => e.Code))}.");
                return MetadataResult.Failed(errors.ToArray());
            }
            return MetadataResult.Success;
        }

        /// <summary>
        /// Creates the specified <paramref name="entity"/> in the persistence store.
        /// </summary>
        /// <param name="entity">The entity to create.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
        public virtual async Task<MetadataResult> CreateEntityAsync(MetadataEntity<TKey> entity)
        {
            ThrowIfDisposed();
            if (entity == null)
            {
                throw new ArgumentNullException(nameof(entity));
            }

            var result = await ValidateEntityAsync(entity);
            if (!result.Succeeded)
            {
                return result;
            }

            return await EntityStore.CreateAsync(entity, CancellationToken);
        }

        /// <summary>
        /// Updates the specified <paramref name="entity"/>.
        /// </summary>
        /// <param name="entity">The type to update.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="MetadataResult"/> for the update.</returns>
        public virtual async Task<MetadataResult> UpdateTypeAsync(MetadataEntity<TKey> entity)
        {
            ThrowIfDisposed();
            if (entity == null)
            {
                throw new ArgumentNullException(nameof(entity));
            }

            var result = await ValidateEntityAsync(entity);
            if (!result.Succeeded)
            {
                return result;
            }

            return await EntityStore.UpdateAsync(entity, CancellationToken);
        }

        /// <summary>
        /// Deletes the specified <paramref name="entity"/>.
        /// </summary>
        /// <param name="entity">The entity to delete.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="MetadataResult"/> for the delete.</returns>
        public virtual Task<MetadataResult> DeleteTypeAsync(MetadataEntity<TKey> entity)
        {
            ThrowIfDisposed();
            if (entity == null)
            {
                throw new ArgumentNullException(nameof(entity));
            }

            return EntityStore.DeleteAsync(entity, CancellationToken);
        }

        /// <summary>
        /// Finds the type associated with the specified <paramref name="entityId"/> if any.
        /// </summary>
        /// <param name="entityId">The entity ID whose type should be returned.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the type associated with the specified <paramref name="entityId"/></returns>
        public virtual Task<MetadataEntity<TKey>> FindTypeByIdAsync(TKey entityId)
        {
            ThrowIfDisposed();
            return EntityStore.FindByIdAsync(entityId, CancellationToken);
        }

        /// <summary>
        /// Finds the type associated with the specified <paramref name="entityName"/> if any.
        /// </summary>
        /// <param name="entityName">The name of the entity to be returned.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the entity associated with the specified <paramref name="entityName"/></returns>
        public virtual Task<MetadataEntity<TKey>> FindTypeByNameAsync(string entityName)
        {
            ThrowIfDisposed();
            if (entityName == null)
            {
                throw new ArgumentNullException(nameof(entityName));
            }

            return EntityStore.FindByNameAsync(entityName, CancellationToken);
        }

        /// <summary>
        /// Returns a list of metadata entities from the type store who associated with the specified property name and property value.
        /// </summary>
        /// <param name="propertyName">The name of the type property.</param>
        /// <param name="propertyValue">The value of the type property.</param>
        /// <returns>A <see cref="Task{TResult}"/> that represents the result of the asynchronous query, a list of <see cref="MetadataEntity{TKey}"/>s who contain the specified property.</returns>
        public virtual Task<IList<MetadataEntity<TKey>>> FindTypesByPropertyAsync(string propertyName, string propertyValue)
        {
            ThrowIfDisposed();
            if (propertyName == null)
            {
                throw new ArgumentNullException(nameof(propertyName));
            }

            if (propertyValue == null)
            {
                throw new ArgumentNullException(nameof(propertyValue));
            }

            return EntityStore.FindByPropertyAsync(propertyName, propertyValue, CancellationToken);
        }

        /// <summary>
        /// Creates the specified <paramref name="field"/> in the persistence store.
        /// </summary>
        /// <param name="entityId">The primary key of the entity that the field belongs to.</param>
        /// <param name="field">The field to create.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
        public virtual async Task<MetadataResult> CreateFieldAsync(TKey entityId, MetadataField<TKey> field)
        {
            ThrowIfDisposed();

            if (field == null)
            {
                throw new ArgumentNullException(nameof(field));
            }

            var result = await ValidateFieldAsync(entityId, field);
            if (!result.Succeeded)
            {
                return result;
            }

            return await FieldStore.CreateAsync(entityId, field, CancellationToken);
        }

        /// <summary>
        /// Updates the specified <paramref name="field"/>.
        /// </summary>
        /// <param name="entityId">The primary key of the entity that the field belongs to.</param>
        /// <param name="field">The field to update.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="MetadataResult"/> for the update.</returns>
        public virtual async Task<MetadataResult> UpdateFieldAsync(TKey entityId, MetadataField<TKey> field)
        {
            ThrowIfDisposed();
            if (field == null)
            {
                throw new ArgumentNullException(nameof(field));
            }

            var result = await ValidateFieldAsync(entityId, field);
            if (!result.Succeeded)
            {
                return result;
            }

            return await FieldStore.UpdateAsync(entityId, field, CancellationToken);
        }

        /// <summary>
        /// Deletes the specified <paramref name="field"/>.
        /// </summary>
        /// <param name="entityId">The primary key of the entity that the field belongs to.</param>
        /// <param name="field">The field to delete.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="MetadataResult"/> for the delete.</returns>
        public virtual Task<MetadataResult> DeleteFieldAsync(TKey entityId, MetadataField<TKey> field)
        {
            ThrowIfDisposed();
            if (field == null)
            {
                throw new ArgumentNullException(nameof(field));
            }

            return FieldStore.DeleteAsync(entityId, field, CancellationToken);
        }

        /// <summary>
        /// Finds field type associated with the specified <paramref name="fieldId"/> if any.
        /// </summary>
        /// <param name="fieldId">The field ID whose field should be returned.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the field associated with the specified <paramref name="fieldId"/></returns>
        public virtual Task<MetadataField<TKey>> FindFieldByIdAsync(TKey fieldId)
        {
            ThrowIfDisposed();
            return FieldStore.FindByIdAsync(fieldId, CancellationToken);
        }

        /// <summary>
        /// Finds the type associated with the specified <paramref name="fieldName"/> if any, belongs to the specified entity.
        /// </summary>
        /// <param name="entityId">The primary key of the entity that the field belongs to.</param>
        /// <param name="fieldName">The name of the field to be returned.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the field associated with the specified <paramref name="fieldName"/></returns>
        public virtual Task<MetadataField<TKey>> FindFieldByNameAsync(TKey entityId, string fieldName)
        {
            ThrowIfDisposed();
            if (fieldName == null)
            {
                throw new ArgumentNullException(nameof(fieldName));
            }

            return FieldStore.FindByNameAsync(entityId, fieldName, CancellationToken);
        }

        /// <summary>
        /// Returns a list of metadata entities from the field store who associated with the specified property name and property value.
        /// </summary>
        /// <param name="entityId">The primary key of the entity that the fields belong to.</param>
        /// <param name="propertyName">The name of the type property.</param>
        /// <param name="propertyValue">The value of the type property.</param>
        /// <returns>A <see cref="Task{TResult}"/> that represents the result of the asynchronous query, a list of <see cref="MetadataField{TKey}"/>s who contain the specified property.</returns>
        public virtual Task<IList<MetadataField<TKey>>> FindFieldsByPropertyAsync(TKey entityId, string propertyName, string propertyValue)
        {
            ThrowIfDisposed();
           if (propertyName == null)
            {
                throw new ArgumentNullException(nameof(propertyName));
            }

            if (propertyValue == null)
            {
                throw new ArgumentNullException(nameof(propertyValue));
            }

            return FieldStore.FindByPropertyAsync(entityId, propertyName, propertyValue, CancellationToken);
        }

        /// <summary>
        /// Gets a list of metadata fields that belong to the specified <paramref name="entityId"/>.
        /// </summary>
        /// <param name="entityId">The primary key of the entity that the fields belongs to.</param>
        /// <returns>A <see cref="Task{TResult}"/> that represents the result of the asynchronous query, a list of <see cref="MetadataField{TKey}"/> who belong to the specified <paramref name="entityId"/>.</returns>
        public virtual Task<IList<MetadataField<TKey>>> GetFieldsInTypeAsync(TKey entityId)
        {
            ThrowIfDisposed();
            return FieldStore.GetFieldsInTypeAsync(entityId, CancellationToken);
        }
    }
}